In [67]:
import numpy as np
import pandas as pd
import librosa
from sklearn.mixture import GaussianMixture
import os
from scipy.signal import spectrogram
import matplotlib.pyplot as plt
from sklearn.model_selection import GridSearchCV
import plotly.express as px
import joblib
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import ConfusionMatrixDisplay
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import confusion_matrix
import plotly
from sklearn.metrics import f1_score
from matplotlib.pyplot import imshow
from IPython.display import clear_output
import pickle
from sklearn.metrics import balanced_accuracy_score
from sklearn.metrics import accuracy_score

Czym jest MFCC?¶

MFCC to metoda ekstrakcji cech z sygnału, która najlepiej sprawdza się w przypadku nagrań mowy. Dzięki MFCC jesteśmy w stanie wyciągnąć z sygnału najważniejsze informacje, na przykład w kontekście rozpoznawania osoby mówiącej. Tak przetworzony sygnał idealnie nadaje się do trenowania modeli uczenia maszynowego.

Jak działa MFCC?¶

  1. Wczytanie spróbkowanego sygnału i wykonanie na nim FFT
    • dokonujemy podziału na ramki czasowe, na których następnie wykonujemy DFT,
    • każdą ramkę czasową możemy pomnożyć przez okno, aby zniwelować artefakty w sygnale, np. przeciek spektralny,
  2. Na sygnał nakładane są trójkątne filtry zwane filtrami melowymi. Charakteryzują się tym, że w niskich częstotliwościach są gęściej ułożone, im wyższe częstotliwości, tym rzadziej są rozmieszczone. To ma odpowiadać logarytmicznemu sposobowi odbierania sygnału przez człowieka (różnica pomiędzy 200 a 400 Hz jest duża, natomiast między 4000 a 4200 Hz jest niewielka),
  3. Sumujemy energię sygnałów dla każdego filtra melowego i logarytmujemy te wartości,
  4. Przeprowadzamy DCT (Discrete Cosine Transform). Ustalamy liczbę współczynników, które chcemy otrzymać, a DCT zadba o to, aby wszystkie były ortogonalne, dzięki czemu nasze dane nie będą liniowo zależne. Dodatkowo, pierwsze współczynniki zawierają najwięcej informacji o naszym sygnale,

In [216]:
signal, sr = librosa.load('C:/Users/zbugo/Desktop/praktyki_zadania/8/train-clean-100/LibriSpeech/train-clean-100/19/198/19-198-0000.flac', sr=16000)
T = len(signal)/sr

t_domain = np.linspace(start=0, stop=T, num=int(T*sr))
f_domain = np.linspace(start=0, stop=sr, num=int(T*sr))

plt.figure(figsize=(18,8));
plt.plot(t_domain, signal);
plt.xlabel('Time (s)');
plt.ylabel('Amplitude');
plt.title('The signal plot in the time domain.');
No description has been provided for this image

Zobaczmy jak wygląda efekt zastosowania MFCC.

In [217]:
mfcc_signal = librosa.feature.mfcc(y=signal, sr=sr, n_mfcc=10)
mfcc_signal = pd.DataFrame(mfcc_signal).T
mfcc_signal.head()
Out[217]:
0 1 2 3 4 5 6 7 8 9
0 -480.398254 88.518921 6.294618 15.530994 5.776135 1.339412 4.857187 4.894787 -3.147498 1.829151
1 -452.700043 86.249115 7.805048 18.683903 4.468122 2.650884 2.545521 8.678699 -2.948222 3.438681
2 -450.530640 84.936935 7.238911 21.303696 3.985613 0.029457 -3.240170 8.618133 -2.353383 6.406995
3 -448.755920 87.868042 6.041327 22.466248 2.228760 -1.586451 -4.000550 6.043972 -4.085260 7.642674
4 -448.149719 86.562126 4.968757 16.408735 1.768439 0.174072 -5.357673 5.279818 -3.902598 6.436638

Korzystając z funkcji mfcc z biblioteki librosa, otrzymujemy macierz MFCC (dla czytelności raportu wyświetlam tylko pięć pierwszych wierszy). Dokonałem transpozycji, ponieważ dzięki temu łatwiej jest zrozumieć otrzymany wynik. Każda kolumna to nowo powstały współczynnik MFCC, a wiersze odpowiadają ramkom czasowym. Dzięki DCT kolumny nie są między sobą liniowo zależne (nie są skorelowane), a dodatkowo, dzięki zastosowaniu DCT, wiemy, że pierwsze kolumny zawierają najwięcej informacji o sygnale. Skutek zastosowania DCT jest podobny do PCA, które dobrze znam ze studiów.

In [218]:
S = librosa.feature.melspectrogram(y=signal, sr=sr, n_mels=512)

plt.figure(figsize=(18,8))
librosa.display.specshow(librosa.power_to_db(S, ref=np.max),
                               x_axis='time', y_axis='mel');
plt.title('Mel-spectrogram');
plt.colorbar();
No description has been provided for this image

Możemy zobaczyć, jak wygląda mel-spektrogram. Jest to ostatni etap w algorytmie MFCC, w którym jesteśmy w stanie wyciągnąć wnioski o sygnale — poniżej wyświetlę zwykły spektrogram dla porównania, podobieństwo jest widoczne od razu. Po przeprowadzeniu DCT nie będziemy już w stanie powiedzieć czegokolwiek o danych, ale będą one przygotowane do uczenia maszynowego, na czym nam bardziej zależy.

In [219]:
f, t, Sxx = spectrogram(signal, sr)
plt.figure(figsize=(18,8))
plt.pcolormesh(t, f, np.log10(Sxx), shading='gouraud');
plt.ylabel('Frequency (Hz)');
plt.xlabel('Time (s)');
plt.title('Ordinary martix');
plt.show();
No description has been provided for this image
In [220]:
plt.figure(figsize=(18,8));
plt.pcolormesh(mfcc_signal);
plt.xlabel('Mel-frequency cepstrum coefficients');
plt.ylabel('Time frames');
plt.title('MFCC matrix');
plt.colorbar();
No description has been provided for this image

Z macierzy MFCC nie da się wyciągnąć żadnych wniosków.

Przygotowanie danych i trening modelu wraz z jego walidacją.¶

Podział danych na zbiór uczący i testowy.¶

Tworzę ramki danych, w których przechowywane są płeć oraz ID speakerów.

In [87]:
#Ścieżka do pliku tekstowego, w którym znajdują się informacje o mówcach
file_path = 'C:/Users/zbugo/Desktop/praktyki_zadania/8/train-clean-100/LibriSpeech/SPEAKERS.TXT'

#Tworzę ramkę danych, w której znajdują się informacje o płci danego mówcy
df = pd.read_csv(file_path, sep='|', comment=';', usecols=[0, 1], 
                 names=["ID", "SEX"])



#Ścieżka do folderu, w którym znajdują się katalogi z nagraniami osób
file_path = 'C:/Users/zbugo/Desktop/praktyki_zadania/8/train-clean-100/LibriSpeech/train-clean-100/'

#Wyciągam wszystkie nazwy podfolderów z powyższej ścieżki (są to ID nagranych osób)
subfolders = [f.name for f in os.scandir(file_path) if f.is_dir()]

#Sortuję ID nagranych osób (najpierw muszę zamienić ID na liczbę)
subfolders = sorted([int(item) for item in subfolders])
subfolders = np.array(subfolders)

df = df.loc[np.isin(np.array(df['ID']), subfolders)]

#Tworzę oddzielne ramki dla kobiet i mężczyzn
df_woman = df[df['SEX'] == ' F '].reset_index(drop=True)
df_man = df[df['SEX'] == ' M '].reset_index(drop=True)
In [96]:
df_woman.head()
Out[96]:
ID SEX
0 19 F
1 32 F
2 39 F
3 40 F
4 83 F

Dokonuję podziału na zbiory treningowe i testowe ze względu na osoby, a nie na nagrania, ponieważ zapewnia to, że w zbiorze treningowym nie będzie nagrań osób ze zbioru testowego i odwrotnie.

In [88]:
train_size=0.8

#kobiety
train_woman = np.random.choice(np.array(df_woman.index), size=int(train_size*len(df_woman)), replace=False)
test_woman = ~np.isin(df_woman.index, train_woman)

df_woman_train = df_woman.loc[train_woman]
df_woman_test = df_woman.loc[test_woman]



#mężczyźni
train_man = np.random.choice(np.array(df_man.index), size=int(train_size*len(df_man)), replace=False)
test_man = ~np.isin(df_man.index, train_man)

df_man_train = df_man.loc[train_man]
df_man_test = df_man.loc[test_man]

df_man_train.head()
Out[88]:
ID SEX
121 8630 M
27 1034 M
92 6437 M
118 8580 M
15 405 M

W ramkach danych zbiorów treningowych i testowych znajdują się informacje o ID osoby oraz płci.

Tworzę listy dla zbiorów treningowegcyh i testowych, w których przechowywane są ścieżki do nagrań.

In [89]:
#Zamieniam ID z powrotem na string
subfolders = [str(item) for item in subfolders]

#Tworzę ścieżki do nagranych osób — do ścieżki train-clean-100/LibriSpeech/train-clean-100/ dodaję ID
full_paths = [file_path + subfolder + '/' for subfolder in subfolders]

full_paths

#Tworzę listy do przechowywania pełnych ścieżek do nagrań i ID osób nagranych
woman_paths_train = []
woman_paths_test = []

man_paths_train = []
man_paths_test = []

for full_path in full_paths:
    #Pobieram nazwy folderów znajdujących się w folderze każdej osoby — nazwa to ID (są tam kolejne foldery)
    subfolders = [f.name for f in os.scandir(full_path) if f.is_dir()]
    #Do ścieżki dodaję nazwy tych podfolderów, aby można było w nie wejść
    subfolders = [full_path + subfolder + '/' for subfolder in subfolders]
    
    for subfolder in subfolders:
        #Zagłębiam się w podfoldery każdej z osób, gdzie znajdują się już pliki z nagraniami. Pobieram ich nazwy i tworzę pełne ścieżki do nagrań
        files = [f.name for f in os.scandir(subfolder) if f.is_file() and f.name.endswith('.flac')]
        ID = files[0].split('-')[0]
        full_path_file = [subfolder + file for file in files]

        #W zależności od płci nagranej osoby, umieszczam ściężki w odpowiednich listach
        if np.isin(ID, df_woman_train['ID']):
            woman_paths_train.extend(full_path_file)
        elif np.isin(ID, df_woman_test['ID']):
            woman_paths_test.extend(full_path_file)
        elif np.isin(ID, df_man_train['ID']):
            man_paths_train.extend(full_path_file)
        else:
            man_paths_test.extend(full_path_file)

man_paths_train[0]
Out[89]:
'C:/Users/zbugo/Desktop/praktyki_zadania/8/train-clean-100/LibriSpeech/train-clean-100/26/495/26-495-0000.flac'

Mając ścieżki do nagrań, biorę każde nagranie po kolei i obliczam dla nich MFCC.

In [90]:
#Liczba współczynników, które otrzymamy po przeprowadzeniu MFCC — ta ilość współczynników powstaje w wyniku DCT (dyskretnej transformacji kosinusowej)
quantity_of_mel_coef = 13
#Liczba filtrów melowych — ilość 'czapek', które zostaną nałożone na sygnał. Dla każdego z nich będzie sumowana energia
quantity_of_mel_filters = 26


#Tworze listy na które będą zbiorami treningowymi, podejście inne niż wczęsniej ponieważ będę chciał liczyć średnie wartości MFCC dla każdego nagrania
train_data_man = []
train_data_woman = []
test_data_man = []
test_data_woman = []

#W każdym obrocie pętli licze średnie wartości MFCC dla każdego nagrania ze zbioru uczącego i dorzucam je do listy 
for i in range(0, len(man_paths_train)):
    signal, sr = librosa.load(man_paths_train[i], sr = 16000)
    mfcc_signal = librosa.feature.mfcc(y=signal, sr=sr, n_mfcc=quantity_of_mel_coef, fmin = 70, n_mels = quantity_of_mel_filters).T
    mfcc_signal = pd.DataFrame(mfcc_signal)
    train_data_man.append(mfcc_signal)

    print(i/len(man_paths_train))
    clear_output(wait=True)
    

for i in range(0, len(woman_paths_train)):
    signal, sr = librosa.load(woman_paths_train[i], sr = 16000)
    mfcc_signal = librosa.feature.mfcc(y=signal, sr=sr, n_mfcc=quantity_of_mel_coef, fmin = 70, n_mels = quantity_of_mel_filters).T
    mfcc_signal = pd.DataFrame(mfcc_signal)
    train_data_woman.append(mfcc_signal)
    
    print(1 + i/len(woman_paths_train))
    clear_output(wait=True)


for i in range(0, len(man_paths_test)):
    signal, sr = librosa.load(man_paths_test[i], sr = 16000)
    mfcc_signal = librosa.feature.mfcc(y=signal, sr=sr, n_mfcc=quantity_of_mel_coef, fmin = 70, n_mels = quantity_of_mel_filters).T
    mfcc_signal = pd.DataFrame(mfcc_signal)
    test_data_man.append(mfcc_signal)

    print(2 + i/len(man_paths_test))
    clear_output(wait=True)


for i in range(0, len(woman_paths_test)):
    signal, sr = librosa.load(woman_paths_test[i], sr = 16000)
    mfcc_signal = librosa.feature.mfcc(y=signal, sr=sr, n_mfcc=quantity_of_mel_coef, fmin = 70, n_mels = quantity_of_mel_filters).T
    mfcc_signal = pd.DataFrame(mfcc_signal)
    test_data_woman.append(mfcc_signal)

    print(3 + i/len(woman_paths_test))
    clear_output(wait=True)
    

train_data_man[0].head()
Out[90]:
0 1 2 3 4 5 6 7 8 9 10 11 12
0 -170.249405 -5.689214 -9.652841 15.865627 1.994629 -0.579982 5.193301 2.992190 -1.422832 2.266842 3.639833 3.930732 -0.570100
1 -157.280792 -1.237496 -7.154668 15.871822 1.078537 -0.898446 4.889987 6.557333 0.881131 2.471999 0.707886 2.463084 -0.065614
2 -165.076233 -2.025683 -5.391717 16.935638 -1.336657 -6.196146 5.163920 8.152512 4.123321 0.872430 1.534155 1.898084 0.280382
3 -177.547180 -4.650189 -4.918454 16.302254 -3.561206 -11.247715 3.163842 6.302247 0.892067 0.698526 3.540739 5.112343 2.828024
4 -190.245193 0.532216 2.830900 17.610018 -0.502290 -13.858301 0.979329 4.836549 -2.789766 2.041788 6.140528 7.485679 5.496440

Listy składają się z ramek danych, gdzie każda ramka to oddzielne nagranie. Kolumny odpowiadają współczynnikom MFCC, a wiersze każdej ramki reprezentują kolejne ramki czasowe.

Skaluję dane, ponieważ wymaga tego model regresji logistycznej.

In [100]:
common_man = pd.concat(train_data_man, ignore_index=True)
common_woman = pd.concat(train_data_woman, ignore_index=True)
common_df = pd.concat([common_man, common_woman], ignore_index=True)

scaler = StandardScaler()
scaler.fit(common_df)
Out[100]:
StandardScaler()
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
StandardScaler()

Model regresji logistycznej będę trenować na średnich wartościach MFCC z nagrań.

In [101]:
df_man_train_lr = pd.DataFrame()
df_woman_train_lr = pd.DataFrame()

#w pętli licze średnie wartości MFCC dla nagrań te dane w jedną ramkę danych - każdy wiersz to kolejne nagranie
for i in range(0, len(train_data_man)):
    means = pd.DataFrame(pd.DataFrame(scaler.transform(train_data_man[i])).mean()).T
    df_man_train_lr = pd.concat([df_man_train_lr, means], ignore_index=True)

df_man_train_lr = pd.concat([df_man_train_lr, pd.DataFrame(['M'] * len(train_data_man))], axis=1, ignore_index=True)


#robię dokładnie to samo, z tym że z nagraniami kobiet
for i in range(0, len(train_data_woman)):
    means = pd.DataFrame(pd.DataFrame(scaler.transform(train_data_woman[i])).mean()).T
    df_woman_train_lr = pd.concat([df_woman_train_lr, means], ignore_index=True)

df_woman_train_lr = pd.concat([df_woman_train_lr, pd.DataFrame(['F'] * len(train_data_woman))], axis=1, ignore_index=True)

df_man_train_lr.head()
Out[101]:
0 1 2 3 4 5 6 7 8 9 10 11 12 13
0 -0.104253 -0.353113 -0.004862 -0.049227 -0.006472 -0.562420 1.016364 0.335728 -0.257635 0.573864 0.277534 0.289206 0.321275 M
1 0.213431 -0.421556 -0.121634 -0.106059 0.055420 -0.447684 0.817728 0.059560 -0.324897 0.287010 0.344723 0.174957 0.275582 M
2 0.210562 -0.578750 -0.226984 -0.082129 -0.049957 -0.337838 0.844517 -0.061543 -0.317499 0.366173 0.614487 0.039263 0.280807 M
3 0.266425 -0.675428 -0.180474 0.007415 0.222433 -0.613701 0.738632 0.161179 -0.283523 0.336112 0.323766 0.110848 0.870306 M
4 0.244404 -0.521116 -0.253386 0.169052 -0.107155 -0.566059 0.775734 0.102443 -0.423973 0.430878 0.340751 0.242681 0.626939 M

Każda kolumna odpowiada współczynnikom MFCC, natomiast każdy wiersz zawiera średnie wartość MFCC dla nagrania.

In [102]:
#łączę w jedną dużą ramkę - wcześniej miałem oddzielną ramkę dla mężczyzn i oddzielną dla kobiet
common_df_train = pd.concat([df_man_train_lr, df_woman_train_lr], ignore_index=True)
common_df_train = pd.get_dummies(common_df_train, drop_first=True)
y_lr = common_df_train.pop('13_M')

Również w zbiorach testowych obliczam średnie wartości MFCC dla nagrań.

In [103]:
#powtarzam czynność z tym że dla zbiorów testowych - licze śrendnie MFCC nagrań

df_man_test_lr = pd.DataFrame()
df_woman_test_lr = pd.DataFrame()

for i in range(0, len(test_data_man)):
    means = pd.DataFrame(pd.DataFrame(scaler.transform(test_data_man[i])).mean()).T
    df_man_test_lr = pd.concat([df_man_test_lr, means], ignore_index=True)

df_man_test_lr = pd.concat([df_man_test_lr, pd.DataFrame(['M'] * len(test_data_man))], axis=1, ignore_index=True)

for i in range(0, len(test_data_woman)):
    means = pd.DataFrame(pd.DataFrame(scaler.transform(test_data_woman[i])).mean()).T
    df_woman_test_lr = pd.concat([df_woman_test_lr, means], ignore_index=True)

df_woman_test_lr = pd.concat([df_woman_test_lr, pd.DataFrame(['F'] * len(test_data_woman))], axis=1, ignore_index=True)

df_woman_test_lr.head()
Out[103]:
0 1 2 3 4 5 6 7 8 9 10 11 12 13
0 0.229237 -0.481813 0.628136 -0.245413 0.207713 -0.456548 0.071745 0.188065 0.606912 -0.048799 -0.481227 0.527952 0.104730 F
1 0.339344 -0.169400 0.146344 0.038569 0.195554 -0.545848 -0.122987 0.105016 0.350086 0.006262 -0.385528 0.547178 0.337483 F
2 0.347223 -0.362980 0.387445 -0.155930 0.177285 -0.215716 -0.051773 0.086410 0.318737 0.019517 -0.489586 0.765157 0.317257 F
3 0.337466 -0.310650 0.545552 -0.009039 0.102142 -0.386599 -0.212607 0.304948 0.202664 -0.203166 -0.520433 0.750297 0.051986 F
4 0.235751 -0.419057 0.335087 -0.108556 0.038642 -0.176796 0.025593 0.070050 0.278399 -0.022571 -0.446516 0.563876 0.173741 F

Analogicznie jak wcześniej, kolumny to współczynniki MFCC, a każdy wiersz zawiera średnie wartości MFCC dla nagrania.

In [104]:
#znów łączę w jedną dużą ramkę z kobietami i mężczyznami
common_df_test = pd.concat([df_man_test_lr, df_woman_test_lr], ignore_index=True)
common_df_test = pd.get_dummies(common_df_test, drop_first=True)
y_lr_test = common_df_test.pop('13_M')

Do wyboru najlepszego modelu użyję przeszukiwania siatki hiperparametrów i dziesięciokrotnej walidacji krzyżowej.

In [105]:
#siatka hiperparametrów dla modelu regresji liniowej
param_grid = {
    'C': np.arange(0.1, 1, step=0.1).tolist(),  # Upewnij się, że C > 0
    'penalty': ['l2', 'none']
}
In [106]:
#uczę model dokunując przeszukania sitaki hiperparametrów i robię walidację krzyżową
lr_model = LogisticRegression()
grid_search = GridSearchCV(estimator=lr_model, param_grid=param_grid, cv=10, n_jobs=5)
grid_search.fit(X=common_df_train, y=y_lr)
lr_model = grid_search.best_estimator_
In [224]:
accurcy = lr_model.score(X=common_df_test, y=y_lr_test)
print('accurcy dla modelu regresji logistycznej: ' + str(accurcy))
accurcy dla modelu regresji logistycznej: 0.8547325797199258

Accuracy nie zawsze jest dobrym wskaźnikiem oceny modelu (dlaczego tak jest, wyjaśniam poniżej, przy ocenie modelu GMM). Sugerując się tylko dokładnością, model wydaje się działać całkiem dobrze. Przyjrzyjmy się jednak macierzy pomyłek.

In [226]:
y_pred = lr_model.predict(X=common_df_test)
cm = confusion_matrix(y_lr_test, y_pred)
disp = ConfusionMatrixDisplay(confusion_matrix=cm,
                              display_labels=lr_model.classes_);
disp.plot();
No description has been provided for this image

Poprawnie zaklasyfikowanych kobiet jest 2377, poprawnie zaklasyfikowanych mężczyzn jest 2689. Kobiet zaklasyfikowanych jako mężczyźni jest 552, a mężczyzn zaklasyfikowanych jako kobiety 309.

In [229]:
balanced_accuracy_lr = balanced_accuracy_score(y_true=y_lr_test, y_pred=y_pred)
print('Balanced accuracy dla modelu regresji logistycznej: ' + str(balanced_accuracy_lr))
F1 score dla modelu regresji logistycznej: 0.861997114922263

F1-score jest naprawdę wysoki, co oznacza, że model jest odporny na nierównowagę klas. Jestem całkiem zadowolony z tego, jak model regresji logistycznej dokonuje klasyfikacji (interpretacja tego wskaźnika znajduje się w dalszej części raportu, przy modelu GMM).

Wizualizacje trzech pierwszych MFCC.¶

Po co właściwie to robię? Podczas liczenia MFCC jednym z etapów jest DCT – dyskretna transformata kosinusowa, która gwarantuje, że współczynniki MFCC są liniowo niezależne od siebie, czyli nieskorelowane. Ponadto, dzięki DCT, pierwsze współczynniki MFCC przekazują najwięcej informacji o sygnale. Sugerując się dobrym działaniem modelu regresji logistycznej, możemy przypuszczać, że istnieje separowalność w trzech pierwszych wymiarach MFCC. Aby model regresji logistycznej dobrze działał, w przestrzeni wielowymiarowej musi być możliwe oddzielenie klas od siebie za pomocą hiperpłaszczyzn. Często zdarza się, że trzy pierwsze wymiary wystarczą, aby to zrobić, a na wykresie 3D widoczne są odseparowane od siebie 'chmury', reprezentujące klasy. Nie zawsze jednak tak jest – czasami separowalność występuje dopiero w wyższych wymiarach niż trzy pierwsze. Wtedy model będzie działać dobrze, ale separowalność nie będzie widoczna na wykresie. Przyjrzyjmy się, jak to wygląda w naszym przypadku. Każdy punkt to średnie wartości trzech pierwszych współczynników MFCC.

In [234]:
plotly.offline.init_notebook_mode()
fig = px.scatter_3d(common_df_test, x=0, y=1, z=2, color=y_lr_test)
fig.update_layout(legend_title_text='Is a man')
fig.show()

Niestety, nie widać idealnej separowalności w trzech pierwszych współczynnikach MFCC, więc dobre działanie modelu regresji logistycznej wynika z uwzględnienia więcej niż trzech współczynników MFCC.

Modele GMM.¶

Zrezygnowałem z GridSearchCV, ponieważ chciałem trenować modele na pełnych zbiorach danych. W związku z tym, wykorzystanie przeszukiwania siatki wraz z walidacją krzyżową trwałoby zbyt długo. Zamiast tego wydzielę zbiór walidacyjny, aby na jego podstawie wybrać najlepsze modele, a jedynym hiperparametrem, który będę zmieniać, będzie liczba komponentów.

In [91]:
#ze zbioru uczącego wyciągam zbiór walidacyjny
train_man_gmm = np.random.choice(np.array(range(0,len(train_data_man))), size=int(train_size*len(train_data_man)), replace=False)
valid_man_gmm = ~np.isin(np.array(range(0,len(train_data_man))), train_man_gmm)

train_man_gmm = [train_data_man[i] for i in train_man_gmm]
valid_man_gmm = [train_data_man[i] for i in range(len(valid_man_gmm)) if valid_man_gmm[i]]

df_man_train_gmm = pd.concat(train_man_gmm)




train_woman_gmm = np.random.choice(np.array(range(0,len(train_data_woman))), size=int(train_size*len(train_data_woman)), replace=False)
valid_woman_gmm = ~np.isin(np.array(range(0,len(train_data_woman))), train_woman_gmm)

train_woman_gmm = [train_data_woman[i] for i in train_woman_gmm]
valid_woman_gmm = [train_data_woman[i] for i in range(len(valid_woman_gmm)) if valid_woman_gmm[i]]

df_woman_train_gmm = pd.concat(train_woman_gmm)

Jedynym hiperparametrem, który będę zmieniać, będzie liczba komponentów – przeszukam zakres od 2 do 59. Covariance_type ustawiłem na 'diag', ponieważ znalazłem informację, że 'diag' nadaje się do danych, w których cechy nie są ze sobą zależne - to zapewnia nam DCT które jest wykonywane podczas liczenia MFCC. Tol wybrałem równy 1e-4, aby wartość warunku zbieżności była niewielka – zależy mi na dokładnym modelu. Podobnie w przypadku liczby iteracji, ustawiłem wartość na 500, ponieważ chcę, aby model dobrze się nauczył. Ustawiłem również wartość reg_covar, ponieważ w macierzy kowariancji mogą wystąpić wartości bliskie 0. Init_params wybrałem 'kmeans', ponieważ, jak przeczytałem, może to polepszyć jakość modelu kosztem dłuższego czasu trenowania, ale dla mnie najważniejsza jest jakość.

In [92]:
#mogę wczytać wcześniej policzone modele

#with open('models_list_man.pkl', 'rb') as file:
#    man_model_gmm = pickle.load(file)

#with open('models_list_woman.pkl', 'rb') as file:
#    woman_model_gmm = pickle.load(file)
In [ ]:
#uczę modele GMM
man_model_gmm = []
woman_model_gmm = []

n_components = np.array(range(2, 60))
for n_component in n_components:
    gmm = GaussianMixture(n_components=n_component, covariance_type='diag', tol=1e-4, max_iter=500, reg_covar=1e-6, init_params='kmeans')
    gmm.fit(df_man_train_gmm)
    man_model_gmm.append(gmm)

    gmm = GaussianMixture(n_components=n_component, covariance_type='diag', tol=1e-4, max_iter=500, reg_covar=1e-6, init_params='kmeans')
    gmm.fit(df_woman_train_gmm)
    woman_model_gmm.append(gmm)

Niestety musiałem przerwać działanie skryptu, ponieważ modele liczyły się bardzo długo. Po przerwie skorzystam z tych, które zdążyły się policzyć.

Wybór najlepszego modelu dokonam na podstawie balanced accuracy, ponieważ jest to lepsza metryka od zwykłego accuracy, która uwzględnia nierównowagę klas.

In [93]:
quantity_of_models = min(len(man_model_gmm), len(woman_model_gmm))

#liczę średnei wartośći log-prawdopodobieństwa z nagrań dla każdego modelu 
#mężczyźni
list_for_balanced_accuracy = []

for i in range(0, quantity_of_models):
    prediction_man = []
    for j in range(0,len(valid_man_gmm)):
        #tu muszę wszystkie obserwacje z modelu walidacyjnego zaklasyfikwać 
        man_likelihood = man_model_gmm[i].score(valid_man_gmm[j])
        woman_likelihood = woman_model_gmm[i].score(valid_man_gmm[j])
        if man_likelihood > woman_likelihood:
            prediction_man.append(True)
        else:
            prediction_man.append(False)


#robię to samo z tym że z mężczyznami
    prediction_woman = []
    for j in range(0,len(valid_woman_gmm)):
        man_likelihood = man_model_gmm[i].score(valid_woman_gmm[j])
        woman_likelihood = woman_model_gmm[i].score(valid_woman_gmm[j])
        if man_likelihood >= woman_likelihood:
            prediction_woman.append(True)
        else:
            prediction_woman.append(False)

    prediction = prediction_man + prediction_woman
    y_tr = [True] * len(valid_man_gmm) + [False] * len(valid_woman_gmm)
    balanced_accuracy = balanced_accuracy_score(y_true=y_tr, y_pred=prediction)
    list_for_balanced_accuracy.append(balanced_accuracy)

Po wyliczeniu balanced accuracy mogę zwizualizować wartość metryki w zależności od liczby komponentów.

In [94]:
plt.figure(figsize=(18,8))
plt.plot(np.array(range(2, quantity_of_models+2)), list_for_balanced_accuracy);
plt.plot(np.argmax(list_for_balanced_accuracy)+2, max(list_for_balanced_accuracy), 'ro', 
         label='najlepsza liczba komponentów: ' + str(np.argmax(list_for_balanced_accuracy)+2) + ' z balnced accuracy na poziomie: ' + str(np.max(list_for_balanced_accuracy)))
plt.xlabel('n_components');
plt.ylabel('balanced accuracy');
plt.title('Zależność jakości modelu od liczby komponentów GMM');
plt.legend();
No description has been provided for this image

Na wykresie widzimy zależność pomiędzy wartością balanced accuracy a liczbą komponentów. Oczywiście im wyższa wartość balanced accuracy, tym lepszy jest model. W naszym przypadku najlepsza liczba komponentów to 51. Możliwe, że gdybyśmy zwiększyli tę liczbę, jakość modelu byłaby jeszcze wyższa choć różnica jest niewielka.

Widzimy, że wraz ze wzrostem liczby komponentów jakość modelu się poprawia. Jednak trenowanie modelu z bardzo dużą liczbą komponentów jest czasochłonne. Niestety, nie ma rzeczy idealnych, więc w tym przypadku trzeba znaleźć pewien kompromis. Wraz ze wzrostem liczby komponentów wykres coraz bardziej się wypłaszcza, co oznacza, że korzyść z dalszego ich zwiększania staje się coraz mniejsza, a czas trenowania modelu znacznie się wydłuża. Moglibyśmy iteracyjnie sprawdzać, czy kolejny model nie jest bardzo podobny do 5 poprzednich. Jeżeli przez te 5 modeli balanced accuracy nie ulega znacznej poprawie (np. o wartość 0.01) lub nawet spada, to oznacza, że nie ma sensu dodawać kolejnych komponentów.

Wybieram najlepsze modele.

In [100]:
best_woman_model = woman_model_gmm[np.argmax(list_for_balanced_accuracy)]
best_man_model = man_model_gmm[np.argmax(list_for_balanced_accuracy)]

test_man_likelihood = []
test_woman_likelihood = []
prediction_man_test = []

for i in range(0,len(test_data_man)):
    test_man_likelihood = best_man_model.score(test_data_man[i])
    test_woman_likelihood = best_woman_model.score(test_data_man[i])
    if test_man_likelihood > test_woman_likelihood:
        prediction_man_test.append(True)
    else:
        prediction_man_test.append(False)



prediction_woman_test = []

for i in range(0,len(test_data_woman)):
    test_man_likelihood = best_man_model.score(test_data_woman[i])
    test_woman_likelihood = best_woman_model.score(test_data_woman[i])
    if test_man_likelihood >= test_woman_likelihood:
        prediction_woman_test.append(True)
    else:
        prediction_woman_test.append(False)


prediction_test = prediction_man_test + prediction_woman_test
y_tr_test = [True] * len(test_data_man) + [False] * len(test_data_woman)
accuracy_test = accuracy_score(y_true=y_tr_test, y_pred=prediction_test)
In [101]:
print('Dokładność najlepszego modelu: ' + str(accuracy_test))
Dokładność najlepszego modelu: 0.9648163962425278

Model klasyfikuje całkiem dobrze, ale accuracy nie zawsze jest dobrym wskaźnikiem oceny modelu, szczególnie gdy występuje nierównowaga klas, co ma miejsce w naszym przypadku. Każda osoba ma różną liczbę nagrań, więc liczba nagrań kobiet i mężczyzn nie jest równa.

In [99]:
cm = confusion_matrix(y_true=y_tr_test, y_pred=prediction_test)

# Wykres macierzy pomyłek za pomocą Matplotlib
disp = ConfusionMatrixDisplay(confusion_matrix=cm)
disp
disp.plot(cmap='Blues')
plt.show()
No description has been provided for this image

Na przekątnej znajdują się poprawnie zaklasyfikowane obserwacje – poprawnie zaklasyfikowanych mężczyzn jest 2779, a poprawnie zaklasyfikowanych kobiet 2870. Niestety, występują również błędy – 89 mężczyzn zostało zaklasyfikowanych jako kobiety, a 117 kobiet jako mężczyźni.

Aby jeszcze lepiej ocenić model zobaczmy jaką wartość ma balanced accuracy.

In [103]:
balanced_aaccuracy_test = balanced_accuracy_score(y_true=y_tr_test, y_pred=prediction_test)
print('Balanced accuracy najlepszego modelu: ' + str(balanced_aaccuracy_test))
Balanced accuracy najlepszego modelu: 0.9648990931881014

Wartość balanced accuracy jest wysoka, niemal identyczna jak zwykła accuracy. Balanced accuracy uwzględnia różnicę w liczebności obserwacji w klasach, dzięki czemu jest bardziej miarodajna niż zwykła accuracy. W naszym przypadku może wystąpić nierównowaga klas, ponieważ na różne osoby przypada różna liczba nagrań, więc warto uwzględnić zbalansowaną dokładność.

Dlaczego warto używać balanced accuracy? Załóżmy, że mamy bardzo niezbalansowane dane, np. 95% to kobiety, a 5% to mężczyźni. Gdybyśmy w takiej sytuacji stworzyli model, który zawsze będzie przewidywać, że nagranie należy do kobiety, to mimo wszystko uzyskalibyśmy model, który w 95% dobrze rozpoznaje płeć. Dlatego używanie accuracy nie jest najlepszym podejściem.

In [237]:
#wczytuje policzone modele


# Zapisanie listy modeli do pliku
#with open('models_list_man.pkl', 'wb') as file:
#    pickle.dump(man_model_gmm, file)

#with open('models_list_woman.pkl', 'wb') as file:
#    pickle.dump(woman_model_gmm, file)

#with open('model_lr.pkl', 'wb') as file:
#    pickle.dump(lr_model, file)